package com.wbl.train.business.service.impl;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.wbl.train.business.service.DailyTrainSeatService;
import com.wbl.train.business.service.DailyTrainStationService;
import com.wbl.train.common.business.dto.SkTokenDto;
import com.wbl.train.common.business.entity.DailyTrainStationEntity;
import com.wbl.train.common.business.entity.SkTokenEntity;
import com.wbl.train.business.mapper.SkTokenMapper;
import com.wbl.train.business.service.DailySkTokenService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wbl.train.common.exception.ApplicationException;
import com.wbl.train.common.page.PageVo;
import com.wbl.train.common.redisson.enums.RedissonKeyPreEnum;
import com.wbl.train.common.result.ResultCode;
import com.wbl.train.common.utils.SnowUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 秒杀令牌 服务实现类
 * @author 王博纶
 * @since 2024-05-26
 */
@Slf4j
@Service
public class SkTokenServiceImpl extends ServiceImpl<SkTokenMapper, SkTokenEntity> implements DailySkTokenService {

    @Resource
    private DailyTrainSeatService dailyTrainSeatService;
    @Resource
    private DailyTrainStationService dailyTrainStationService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageVo<SkTokenEntity> querySKToken(SkTokenDto skTokenDto) {
        // 获取分页所需要的参数
        int page = skTokenDto.getPage();
        int limit = skTokenDto.getLimit();

        LambdaQueryWrapper<SkTokenEntity> skTokenWrapper = new LambdaQueryWrapper<SkTokenEntity>()
                // 秒杀令牌编号查询
                .eq(ObjectUtil.isNotEmpty(skTokenDto.getTrainCode()),SkTokenEntity::getTrainCode,skTokenDto.getTrainCode())
                // 根据日期查询
                .eq(ObjectUtil.isNotNull(skTokenDto.getDate()),SkTokenEntity::getDate,skTokenDto.getDate())
                .orderByDesc(SkTokenEntity::getDate)
                .orderByAsc(SkTokenEntity::getTrainCode);
        //创建分页对象
        IPage<SkTokenEntity> skTokenPage = new Page<>(page,limit);
        //调用分页方法
        IPage<SkTokenEntity> skTokenPageData = page(skTokenPage,skTokenWrapper);
        // 获取查询出来的数据集合
        List<SkTokenEntity> records = skTokenPageData.getRecords();
        // 将数据塞到PageVo类中并返回出去
        return new PageVo<SkTokenEntity>()
                .setDataList(records)
                .setPageSize(limit).setCurrentPage(page)
                .setTotal((int) skTokenPageData.getTotal())
                .setPages((int) skTokenPageData.getPages());
    }

    @Override
    public boolean insertAndUpdateSKToken(SkTokenEntity skToken) {
        // 获取当前的时间 --- 用于创建和修改的时候的时间填充
        DateTime now = DateTime.now();
        // 设置秒杀令牌的修改时间
        skToken.setUpdateTime(now);
        // 判断Id是否为空
        if(ObjectUtil.isNotNull(skToken.getId())){
            // 根据id查询秒杀令牌
            SkTokenEntity skTokenEntity = this.getById(skToken.getId());
            // 当前id对应的无秒杀令牌
            if(ObjectUtil.isNull(skTokenEntity)){
                throw new ApplicationException(ResultCode.DATA_NOT_EXIST,"秒杀令牌不存在");
            }
            // 更新秒杀令牌
            return this.updateById(skToken);
        }
        // 保存之前的唯一键校验
        SkTokenEntity skTokenEntity = getSKTokenByUniqueKey(skToken.getTrainCode(),skToken.getDate());
        // 查询出的trainEntity不为空，不允许新增
        if(ObjectUtil.isNotEmpty(skTokenEntity)){
            throw new ApplicationException(ResultCode.DATA_EXIST,"秒杀令牌已存在");
        }
        // 给新增的秒杀令牌设置Id，创建时间和修改时间
        skToken.setId(SnowUtil.getSnowflakeNextId())
                .setCreateTime(now);
        // 新增秒杀令牌
        return this.save(skToken);
    }

    @Override
    public void genDaily(LocalDate date,String trainCode) {
        // 获取当前的时间
        DateTime now = DateTime.now();
        // 先删除对应的令牌余量的数据
        removeTokenByUniqueKey(date, trainCode);
        // 计算令牌余量
        int skTokenCount = calculateTokenCount(date, trainCode);
        // 构建令牌余量的对象
        SkTokenEntity skTokenEntity = createTokenEntity(date, trainCode, now, skTokenCount);
        log.info("火车【{}】的令牌对象已经构建完成",trainCode);
        log.info("------准备开始落库了------");
        // 保存令牌余量的数据
        save(skTokenEntity);
        log.info("火车【{}】生成的令牌对象已经保存到数据库",trainCode);
    }

    @Override
    public boolean validSKToken(LocalDate date, String trainCode, Long memberId, int decreaseCount) {
        // 使用redisson分布式锁进行加锁 --- 加锁维度：日期 + 火车编码 + 用户
        String lockKey = RedissonKeyPreEnum.SK_TOKEN.getCode() + "-" + date + "-" + trainCode + "-" + memberId;
        log.info("令牌校验时候的redisson的key");
        RLock lock;
        try{
            // 获取锁的对象
            lock = redissonClient.getLock(lockKey);
            // 尝试加锁
            boolean tryLock = lock.tryLock(5, TimeUnit.SECONDS);
            if(tryLock){
                // 加锁成功
                log.info("key为：{}的分布式锁加锁成功",lockKey);
            }else{
                log.error("key为：{}的分布式锁加锁失败",lockKey);
                // 加锁失败就返回false
                return false;
            }

            SkTokenEntity skToken = getSKTokenByUniqueKey(trainCode,date);
            // 先去查询对应的令牌余量
            int skTokenCount = skToken == null ? 0 : skToken.getCount();
            log.info("日期【{}】车次【{}】对应的令牌数量为：{}",date,trainCode,skTokenCount);
            String skTokenCountKey = RedissonKeyPreEnum.SK_TOKEN_COUNT.getCode() + "-" + date + "-" + trainCode;
            // 从缓存中获取key对应的值
            String redisTokenCount = stringRedisTemplate.opsForValue().get(skTokenCountKey);
            int tokenCount = redisTokenCount == null ? 0 : Integer.parseInt(redisTokenCount);
            boolean updateResult;
            // 缓存中的数量不为0
            if(tokenCount != 0){
                log.info("缓存中有该车次的令牌大闸对应的key：{}",skTokenCountKey);
                // 对缓存中的令牌数量 - 1
                Long decrement = stringRedisTemplate.opsForValue().decrement(skTokenCountKey, 1);
                long count = decrement == null ? 0L : decrement;
                if(count < 0L){
                    log.error("获取令牌失败：{}",skTokenCountKey);
                    return false;
                }else{
                    log.info("获取令牌后，令牌余量：{}",count);
                    // 重置缓存
                    stringRedisTemplate.expire(skTokenCountKey,60,TimeUnit.SECONDS);
                    int updateCount = (int) Math.min(count, Long.valueOf(decreaseCount));
                    // 每获取5个令牌更新一次数据库
                    if(count % decreaseCount == 0){
                        // 更新数据库的余量
                        return updateSKTokenCount(date, trainCode, memberId, updateCount, skTokenCount);
                    }else if(count < decreaseCount){
                        return updateSKTokenCount(date, trainCode, memberId, 1, skTokenCount);
                    }
                }
            }else{
                log.info("缓存中没有存在该车次令牌大闸的key：{}",skTokenCountKey);
                // 检查是否还有令牌
                SkTokenEntity dbSkToken = getSKTokenByUniqueKey(trainCode,date);
                if(dbSkToken == null){
                    log.info("找不到日期【{}】车次【{}】的令牌记录",date,trainCode);
                    return false;
                }
                // 先去查询对应的令牌余量
                int dbSkTokenCount = skToken == null ? 0 : skToken.getCount();
                if(dbSkTokenCount <= 0){
                    log.info("日期【{}】车次【{}】的令牌数量为0",date,trainCode);
                    return false;
                }
                // 数据库中还有令牌的余量
                updateResult = updateSKTokenCount(date, trainCode, memberId, 1, skTokenCount);
                if(updateResult){
                    // 更新成功，将更新后的数量放入缓存中
                    stringRedisTemplate.opsForValue().set(skTokenCountKey, String.valueOf(skTokenCount -1));
                }
                return updateResult;
            }
            return true;
        } catch (Exception e) {
            log.error("key为：{}的分布式锁加锁时候发生异常",lockKey);
            // 发生异常也返回false
            return false;
        }
    }

    /**
     * 更新数据库中的令牌余量的数量
     * @param date 日期
     * @param trainCode 火车编码
     * @param memberId 会员id
     * @param decreaseCount 要扣减的数量
     * @param skTokenCount 库存中剩余的数量
     */
    private boolean updateSKTokenCount(LocalDate date, String trainCode, Long memberId, int decreaseCount, int skTokenCount) {
        // 更新数据库的余量
        log.info("会员【{}】开始尝试更新日期【{}】车次【{}】的令牌", memberId, date, trainCode);
        // 更新数据库中的余票数量 --- 如果余票数量不支持可减少就返回false
        LambdaUpdateWrapper<SkTokenEntity> updateWrapper = new LambdaUpdateWrapper<SkTokenEntity>()
                .eq(SkTokenEntity::getDate, date)
                .eq(SkTokenEntity::getTrainCode, trainCode)
                // 当余票数量 > 参数传递的要扣减的数量 的时候才去更新
                .gt(SkTokenEntity::getCount, decreaseCount)
                .set(SkTokenEntity::getCount, skTokenCount - decreaseCount);
        boolean updateResult = update(updateWrapper);
        log.info("会员【{}】更新日期【{}】车次【{}】的令牌的结果为：{}", memberId, date, trainCode,updateResult);
        return updateResult;
    }

    /**
     * 创建用于落库的令牌余量的对象
     * @param date 日期
     * @param trainCode 火车编号
     * @param now 创建时间和修改时间
     * @param skTokenCount 令牌数量
     */
    private static SkTokenEntity createTokenEntity(LocalDate date, String trainCode, DateTime now, int skTokenCount) {
        // 创建一个令牌余量的对象
        return new SkTokenEntity()
                .setId(SnowUtil.getSnowflakeNextId())
                .setDate(date)
                .setTrainCode(trainCode)
                .setCount(skTokenCount)
                .setCreateTime(now)
                .setUpdateTime(now);
    }

    /**
     * 根据唯一键来删除对应的令牌余量的数据
     * @param date 联合唯一键之一：日期
     * @param trainCode 联合唯一键之二：火车编号
     */
    private void removeTokenByUniqueKey(LocalDate date, String trainCode) {
        log.info("删除日期【{}】车次【{}】的令牌记录", date, trainCode);
        LambdaQueryWrapper<SkTokenEntity> skTokenWrapper = new LambdaQueryWrapper<SkTokenEntity>()
                .eq(SkTokenEntity::getDate, date)
                .eq(SkTokenEntity::getTrainCode, trainCode);
        // 根据日期和火车编号删除对应的令牌余票数据
        remove(skTokenWrapper);
    }

    /**
     * 计算初始化的令牌余量
     * @param date 日期
     * @param trainCode 火车编号
     * @return 令牌余量
     */
    private int calculateTokenCount(LocalDate date, String trainCode) {
        int skTokenCount;
        // 查询指定车次的座位数
        long seatCount = dailyTrainSeatService.countSeat(date,trainCode,null);
        log.info("车次【{}】座位数：【{}】", trainCode,seatCount);

        // 查询指定车次的车站数
        LambdaQueryWrapper<DailyTrainStationEntity> trainStationQWrapper = new LambdaQueryWrapper<DailyTrainStationEntity>()
                .eq(DailyTrainStationEntity::getDate,date)
                .eq(DailyTrainStationEntity::getTrainCode, trainCode);
        long stationCount = dailyTrainStationService.count(trainStationQWrapper);
        log.info("车次【{}】到站数：【{}】", trainCode,stationCount);

        // 根据实际卖票比例来定义，一趟火车最多可卖（seatCount * stationCount）张火车票
        skTokenCount = (int) (seatCount * stationCount);
        log.info("车次【{}】初始生成令牌数：{}", trainCode,skTokenCount);
        return skTokenCount;
    }

    /**
     * 根据唯一键查询 sk_token 秒杀令牌信息
     * @param code 秒杀令牌编码
     * @param date 秒杀令牌当日日期
     * @return sk_token 秒杀令牌信息
     */
    private SkTokenEntity getSKTokenByUniqueKey(String code, LocalDate date) {
        // 通过code进行车站信息的查询
        LambdaQueryWrapper<SkTokenEntity> wrapper = new LambdaQueryWrapper<SkTokenEntity>()
                .eq(SkTokenEntity::getTrainCode, code)
                .eq(SkTokenEntity::getDate,date)
                ;
        return this.getOne(wrapper);
    }
}
